前后端分离应用接入CAS单点登录处理方式

您所在的位置:网站首页 springboot cas 前后端分离 前后端分离应用接入CAS单点登录处理方式

前后端分离应用接入CAS单点登录处理方式

2024-01-21 21:50| 来源: 网络整理| 查看: 265

这段时间一直在处理单点登录的问题,元旦前对接了基于SAML的单点登录认证,这几天又对接了一个基于CAS认证的,认证中心提供的对接文档都默认接入的client应用是前后端不分离的应用,所以踩了很多坑,过程中也找到一些前后端分离认证的共性问题。在此记录一下处理过程。

cas认证大致流程,简单画了个图

未命名文件.png

前后端不分离的应用集成很简单,springboot方式与springsecurity的集成官方文档都有很详细的说明 https://github.com/apereo/java-cas-client

前后端分离的认证,看了别人写的一些方案都不太适合我的场景,要么改动涉及认证中心,要么比较丑陋,比如使用iframe嵌套传递登录信息、后端代码中加一个JSP文件中专等方式,简单尝试了一下就放弃了。

我的处理思路:

1.先对接后台服务。把后台程序单独拿出来,看做是一个前后端不分离的应用,按照前后端不分离的方式对接,浏览器地址栏直接请求后台接口(如订单列表接口),是否对接成功也很容易验证,认证中心登录后,该后台地址的接口在浏览器界面返回了订单列表的数据即认为认证成功了,也就是后端代码对接成功了CAS。这样的好处是可以快速先把CAS集成进来,如果直接在前后端分离的体系下对接,因为本身有前后端分离的问题在里面,很难确认CAS集成是否有问题。

2.接入前端页面。即通过前端页面再来尝试调用该后台接口,启动前端程序,比如点击订单列表页面的查询按钮调用该订单列表接口。后台CAS filter拦截到前端发出的请求,认证校验未通过,返回302重定向的状态码,浏览器拿到302尝试跳转,但是跳转失败了。原因是该请求是前端代码发起的ajax请求,ajax请求无法302跳转,前端代码也无法捕捉跳转,这种情况不用考虑在前端代码中实现跳转了。

3.考虑后端处理。前后端不分离的应用中页面跳转可以直接在后端java代码中用response.sendRedirect()直接跳转到指定地址,这种方式在以前的前后端不分离的JSP项目中没问题,但是前后端分离的项目步行。查看CAS源码,发现CAS认证不通过,跳转到登录页最终也是response.sendRedirect()来实现,所以考虑能否拦截或者重写覆盖CAS跳转相关的代码,让这里的处理不走默认的跳转逻辑,自定义处理方式,返回401状态码给调用方(也就是前端代码),并且带上要跳转的链接,前端可以捕获401状态码的返回结果,获取要跳转的链接后跳转过去。

4.检查是否有新问题引入。实践证明上述思路是可行的,CAS本身的代码设计也非常优雅,提供给了我们覆盖相关逻辑的方式,具体方式请看后面的实现。重写CAS认证失败页面跳转的相关代码,前后端分离应用就可以正常对接CAS了,可能有一些小的细节问题处理,但是没有新的流程阻塞问题引入,如果有新问题,比如跨域,针对性解决即可。

以上是我在处理此类问题时总结出的大致方式,下面说说关键步骤,也就是跳转的处理方式

先说跳转处理方式,再分析

一、未集成springsecurity 代码包版本:

springboot:2.1.6.RELEASE cas-server: 5.3.x cas-client: cas-client-support-springboot:3.6.0

1.定义一个跳转处理类,实现AuthenticationRedirectStrategy接口 import lombok.SneakyThrows; import org.jasig.cas.client.authentication.AuthenticationRedirectStrategy; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.net.URLEncoder; @Component public class CustomAuthRedirectStrategy implements AuthenticationRedirectStrategy { @SneakyThrows @Override public void redirect(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, String s) throws IOException { // 自定义一个后台接口,该controller接口内只写一个response.sendRedirect(应用首页) String dealUrl = "http://cas.app.com/api/1.0/users/loginRedirect"; String encodeUrl = URLEncoder.encode(dealUrl, "utf-8"); // cas认证中心登录页地址 String loginUrl = "http://cas.proaim.com:8080/cas/login" + "?service=" + encodeUrl; httpServletResponse.setStatus(401); PrintWriter out = httpServletResponse.getWriter(); // 格式自定义,前端能获取到loginUrl即可 out.write("{\"errors\":[" + "\"" + loginUrl + "\"" + "]}"); } } 2.修改cas filter初始化参数 import com.proaimltd.web.casclient.filter.CustomAuthRedirectStrategy; import org.jasig.cas.client.boot.configuration.CasClientConfigurer; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Configuration; @Configuration public class CasAuthConfig implements CasClientConfigurer { @Override public void configureAuthenticationFilter(FilterRegistrationBean authenticationFilter) { // 源码中使用反射初始化authenticationRedirectStrategyClass, 用自定义的跳转类覆盖默认的authenticationRedirectStrategyClass authenticationFilter.getInitParameters().put("authenticationRedirectStrategyClass", CustomAuthRedirectStrategy.class.getName()); } }

注意点:此方式实现的跳转,如果ticket认证成功后,跳转回的地址带有;jsessionId=xxxxx,在配置中加上

server.servlet.session.tracking-modes=cookie

原因是应用不确定浏览器是否禁用了cookie,所以用这种方式来传递session到服务端,加上配置等于告诉应用session可以通过cookie来传递

分析:

此方式直接引用了官方提供的springboot client包

org.jasig.cas.client cas-client-support-springboot 3.6.0

代码跟踪到org.jasig.cas.client.authentication.AuthenticationFilter的doFilter方法,可以看到跳转的代码 this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo),是一个接口

// 省略非关键代码 public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)servletRequest; HttpServletResponse response = (HttpServletResponse)servletResponse; if (this.isRequestUrlExcluded(request)) {...} else { if (assertion != null) {...} else { ... if (!CommonUtils.isNotBlank(ticket) && !wasGatewayed) { .... String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, this.getProtocol().getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway, this.method); this.logger.debug("redirecting to \"{}\"", urlToRedirectTo); // 此处跳转 this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo); } else { filterChain.doFilter(request, response); } } } }

构造函数中,authenticationRedirectStrategy接口的默认实现是 DefaultAuthenticationRedirectStrategy

protected AuthenticationFilter(Protocol protocol) { super(protocol); this.renew = false; this.gateway = false; this.gatewayStorage = new DefaultGatewayResolverImpl(); this.authenticationRedirectStrategy = new DefaultAuthenticationRedirectStrategy(); this.ignoreUrlPatternMatcherStrategyClass = null; }

DefaultAuthenticationRedirectStrategy方法内仅有跳转相关的代码,所以可以放心替代

public final class DefaultAuthenticationRedirectStrategy implements AuthenticationRedirectStrategy { public DefaultAuthenticationRedirectStrategy() { } public void redirect(HttpServletRequest request, HttpServletResponse response, String potentialRedirectUrl) throws IOException { response.sendRedirect(potentialRedirectUrl); } }

继续看该filter的代码,找到initInternal方法,该方法在client程序启动时初始化了AuthenticationRedirectStrategy的实现,可以看到此处通过getClass方法获取AuthenticationRedirectStrategy的实现类,

protected void initInternal(FilterConfig filterConfig) throws ServletException { // 省略前面 ..... Class


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3